秒杀项目学习笔记 第一、二章——项目框架搭建 实现登陆功能
redis有多个库,最多16个,默认为0库
第一章:
集成Redis:
1.添加Jedis依赖:
2.添加Fastjson:为了序列化,对象与字符串(json格式)的转化
第二章(实现登陆功能):
1.数据库设计
2.明文密码两次MD5处理
3.JSR303参数检验+全局异常处理器
4.分布式session(重要)
两次MD5(安全)
http是明文传输,用户密码会在网络上传输
1.用户端: PASS = MD5 (明文+固定Salt)
    用户端先MD5后再传输给服务端,防止传输窃取
2.服务端:  PASS = MD5 (用户输入+ 随机Salt)
    接收后,会随机生成salt,与用户md5生成拼装,再做MD5, 结果再写入数据库,放置数据库被盗。防止彩虹表,由一次的MD5反查出密码,所以要再进行一次MD5。
2-2 实现登陆功能
新建了一个LoginVo类
作用:用于在console中输入后台所接收到的mobile和password。
实现:在loginController中引入变量log,使用log.info(loginVo.toString())输出,loginVo就是前端传来的参数
前端:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40<script>
function login(){
	$("#loginForm").validate({
        submitHandler:function(form){
             doLogin();
        }    
    });
}
function doLogin(){
	g_showLoading();//展示loading框
	
	var inputPass = $("#password").val();
	var salt = g_passsword_salt; //在common.js中提供
	var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
	var password = md5(str);  //md5.js提供
	
	$.ajax({
		url: "/login/do_login",
	    type: "POST",
	    data:{
	    	mobile:$("#mobile").val(),
	    	password: password
	    },
	    success:function(data){
	    	layer.closeAll();   //不管成功失败,先关框
            console.log(data);
	    	if(data.code == 0){
	    		layer.msg("成功");
	    		window.location.href="/goods/to_list";
	    	}else{
	    		layer.msg(data.msg);
	    	}
	    },
	    error:function(){
	    	layer.closeAll();
	    }
	});
}
</script>
1  | import org.slf4j.Logger;  | 
2-3 JSR303参数校验 + 全局异常处理器
在登陆的时候,传参的时候需要检验。
若每个页面服务都写loginController里的话,很麻烦
参数校验
1.引入spring-boot-starter-validation依赖
2.给前端传来的参数LoginVo加上@Valid注解
3.给参数LoginVo对象类,所需要验证的属性(如电话,密码)加上校验注解,比如NOTNULL,也可以自己创建符合的注解,比如手机号是1开头,共11位。
4.若要新建注解,应在validator文件夹中新建@注解,并传入所对应的验证类。如IsMobileValidator,IsMobileValidator implements ConstraintValidator,重写初始化和验证方法。
5.
异常拦截处理:
问题:当加上参数校验时,若未通过校验,会返回给浏览器400异常,但是并不会显示,添加异常处理显示,这样对用户更加友好
目的:拦截绑定异常,输出错误信息
结构:
Controller类:负责业务的转发,接收传来的@Valid LoginVo(mobile password已装载)
Service类:负责业务逻辑,包含业务上的校验(手机是否存在,密码是否正确)。校验成功返回true,失败则new GlobalException(CodeMsg)对应异常并抛出
GlobalException类:根据CodeMsg构造,具有CodeMsg属性
GlobalExceptionHandler类: 类名前添加注解@ControllerAdvice,类似切面功能,有exceptionHandler方法,能够捕获异常,根据异常类别,返回不同的Result.error(ex.getCodeMsg())
@Valid:负责入参的格式校验,表明LoginVo(mobile password)受校验,可自定义添加注解校验
IsMobileValidator类:用于实现注解@IsMobile(用于验证手机号)的验证,里面可能会使用到工具类ValidatorUtil来校验。
ValidatorUti类:提供了多种验证方法
2-6 分布式Session
分布式多台服务器,处理用户的Session,
可选方法:1.Session同步(应用很少,因为多服务器同步实现复杂)
所用方法:
1.使用工具类UUID,修改并生成不带“-”的cookie字符串String token = UUIDUtil.uuid();
2.将token保存在redis缓存中,以便于下次验证redisService.set(MiaoshaUserKey.token, token, user); 前缀,key,value
3.将cookie对象加入response,发送回给用户,以便用户下次发送给客户端1
2
3
4Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token); //作为name和value
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);//加入response,
4.验证Session如何实现??
登陆成功后,在login.html中会有ajax异步window.location.href="/goods/to_list";跳转到商品列表,访问/goods/to_list,客户端会将session放在request中发送
1  | /**  | 
最后goods.html通过thymeleaf:
<p th:text="'hello:'+${user.nickname}" ></p>实现Session的更新功能,根据用户最后一次点击时间为起点,在to_list中调用getByToken获取user对象时,若取到了用户,就会重新
addCookie(response, token, user)
分布式Session的优化
在很多的界面跳转时都要验证Session,若在每个方法内都加注解判断token,每个方法都增加根据token获取user的话很冗杂。想到可以将方法抽离出来也需要有没有实现Session更优雅的方式?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16("/to_list")
public String list(HttpServletResponse response, Model model,
                      @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
                      @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken
                      ){
    if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
        return "login";
    }
    String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;//优先取paramToken,为空才取cookieToken
    //根据token从redis中获取用户信息
    MiaoshaUser user = userService.getByToken(response,token);
    model.addAttribute("user", user);
    return "goods_list";
}
能不能变成如下方式?直接就获取到了user,不用根据Token来判断了,需要实现argument resolvor参数处理,mvc框架提供了1
2
3
4
5("/to_list")
public String list(Model model,  MiaoshaUser user){
    model.addAttribute("user", user);
    return "goods_list";
}
这里我们联想到了添加参数model,request,response实现的原理————argumentResolver,  通过WebMvcConfigurerAdapter(WebMVC配置适配器)
实现:
1.新建WebConfig继承WebMvcConfigurerAdapter,加上@Configuration1
2
3
4
5
6
7
8
9
10
11
12
public class WebConfig extends WebMvcConfigurerAdapter{
    
    UserArgumentResolver userArgumentResolver;//这是为了添加user实现的resolver
    
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(userArgumentResolver);//将其加入argumentResolver列表
    }
}
2.新建UserArgumentResolver 实现 HandlerMethodArgumentResolver1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    
    MiaoshaUserService userService;
    //判断是否是要引入的对应类
    
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<?> clazz = methodParameter.getParameterType();
        return clazz == MiaoshaUser.class;
    }
    
    //用于根据各种参数,返回所引入得对象。(就跟引入model一样啦)
    
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
    
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
        String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);//参数中的根据名字就有
        String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);//取放在cookies中的cookie,只取cookie名字对上的
        
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return null;
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        //根据token从redis中获取用户信息
        return userService.getByToken(response,token);
    }
    private String getCookieValue(HttpServletRequest request, String cookiNameToken) {
        //疑问:request.getCookies()会有很多个cookies吗?只取名为MiaoshaUserService.COOKI_NAME_TOKEN的
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(cookiNameToken)) {
                return cookie.getValue();
            }
        }
        return null;
    }
}
![此处输入图片的描述][1]
  [1]: http://pbw0qqogs.bkt.clouddn.com/cookie_token.png
这样,我们的GoodsController就变得异常简洁了。能够直接自动的取user,取不到会直接返回个null的user。
1  | ("/to_list")  |